Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / feature / firmware / src / commonTest / kotlin / org / meshtastic / feature / firmware / ota / Esp32OtaUpdateHandlerTest.kt

Displaying Raw β€’ Download

feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt 9fa9f1e341ee8d2c8fd9fabf9253b2f012fc8c95 (9fa9f1e3) Text, 24.76 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.firmware.ota

Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.everySuspend
Tff7b72import T7ee787dev.mokkery.matcher.any
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787kotlinx.coroutines.CoroutineDispatcher
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.runBlocking
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.meshtastic.core.common.util.CommonUri
Tff7b72import T7ee787org.meshtastic.core.database.entity.FirmwareRelease
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.DeviceHardware
Tff7b72import T7ee787org.meshtastic.core.repository.FirmwareUpdateStatusRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeNodeRepository
Tff7b72import T7ee787org.meshtastic.core.testing.FakeRadioController
Tff7b72import T7ee787org.meshtastic.core.testing.TestDataFactory
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareArtifact
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareFileHandler
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareRetriever
Tff7b72import T7ee787org.meshtastic.feature.firmware.FirmwareUpdateState
Tff7b72import T7ee787org.meshtastic.proto.ClientNotification
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertIs
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Tests for [Esp32OtaUpdateHandler]'s OTA preflight gate β€” the bounded wait for firmware confirmation between
* `triggerRebootOta` and tearing down the mesh transport.
*
* Each test drives `startUpdate` end-to-end just far enough to observe the preflight outcome:
* - Confirmed β†’ mesh disconnect (`setDeviceAddress("n")`) is invoked, then the handler proceeds into the BLE transport
* phase, which fails fast on the mocked BLE deps. We bound the call with `withTimeoutOrNull` so the post-preflight
* BLE-connect retry loop is cancelled before it costs real wall-clock time.
* - Rejected β†’ mesh transport is preserved (no `setDeviceAddress`) and an `Error` state is emitted; the handler returns
* cleanly with no exception to chase.
* - Silent firmware β†’ legacy fallback disconnects the mesh transport and proceeds toward OTA connection attempts.
*
* The firmware response is published from inside [FakeRadioController.requestRebootOta] via a deterministic test hook
* ([FakeRadioController.onRequestRebootOta]). The handler clears stale notifications and captures the baseline BEFORE
* invoking `requestRebootOta` (see `performUpdate`), so the hook always fires after those steps β€” no fixed-delay
* sibling coroutine that can race the handler under slower host timing.
*/
Tff7b72class T56d364Esp32OtaUpdateHandlerTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3release Tff7b72= Te6edf3FirmwareReleaseTb4b4b4(Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffv1.0Ta5d6ff"Tb4b4b4, Te6edf3title Tff7b72= Ta5d6ff"Ta5d6fftestTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3hardware Tff7b72=
Te6edf3DeviceHardwareTb4b4b4(Te6edf3hwModelSlug Tff7b72= Ta5d6ff"Ta5d6ffHELTEC_V3Ta5d6ff"Tb4b4b4, Te6edf3platformioTarget Tff7b72= Ta5d6ff"Ta5d6ffheltec-v3Ta5d6ff"Tb4b4b4, Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffesp32-s3Ta5d6ff"Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3firmwareUri Tff7b72= Te6edf3CommonUriTb4b4b4.Te6edf3parseTb4b4b4(Ta5d6ff"Ta5d6fffile:///tmp/firmware.binTa5d6ff"Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3dispatchers Tff7b72=
Te6edf3CoroutineDispatchersTb4b4b4(
Te6edf3io Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3main Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3default Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffmakeHandlerTb4b4b4(
Te6edf3radioControllerTb4b4b4: Te6edf3FakeRadioControllerTb4b4b4,
Te6edf3nodeRepositoryTb4b4b4: Te6edf3FakeNodeRepositoryTb4b4b4,
Te6edf3fileHandlerTb4b4b4: Te6edf3FirmwareFileHandlerTb4b4b4,
Te6edf3firmwareUpdateStatusRepositoryTb4b4b4: Te6edf3FirmwareUpdateStatusRepositoryTb4b4b4,
Te6edf3environmentTb4b4b4: Te6edf3TestEsp32OtaUpdateEnvironmentTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3Esp32OtaUpdateHandler Tff7b72= Te6edf3Esp32OtaUpdateHandlerTb4b4b4(
Te6edf3firmwareRetriever Tff7b72= Te6edf3FirmwareRetrieverTb4b4b4(Te6edf3fileHandlerTb4b4b4)Tb4b4b4,
Te6edf3firmwareFileHandler Tff7b72= Te6edf3fileHandlerTb4b4b4,
Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3firmwareUpdateStatusRepository Tff7b72= Te6edf3firmwareUpdateStatusRepositoryTb4b4b4,
Te6edf3environment Tff7b72= Te6edf3environmentTb4b4b4,
Te6edf3bleScanner Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3bleConnectionFactory Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3dispatchersTb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffnewFileHandlerTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FirmwareFileHandler Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4) Tb4b4b4{
Te6edf3everySuspend Tb4b4b4{ Te6edf3readBytesTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3byteArrayOfTb4b4b4(T79c0ff1Tb4b4b4, T79c0ff2Tb4b4b4, T79c0ff3Tb4b4b4, T79c0ff4Tb4b4b4)
Te6edf3everySuspend Tb4b4b4{ Te6edf3importFromUriTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3FirmwareArtifactTb4b4b4(Te6edf3uri Tff7b72= Te6edf3firmwareUriTb4b4b4, Te6edf3fileName Tff7b72= Ta5d6ff"Ta5d6fffirmware.binTa5d6ff"Tb4b4b4, Te6edf3isTemporary Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffnewNodeRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FakeNodeRepository Tff7b72= Te6edf3FakeNodeRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3setMyNodeInfoTb4b4b4(Te6edf3TestDataFactoryTb4b4b4.Te6edf3createMyNodeInfoTb4b4b4(Te6edf3myNodeNum Tff7b72= T79c0ff1Tb4b4b4, Te6edf3firmwareVersion Tff7b72= Ta5d6ff"Ta5d6ff1.0Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Shared fixture builder for preflight tests. Publishes [confirmationMessage] from the reboot-request hook as a
* firmware ClientNotification; pass `confirmationMessage = null` to skip the simulated response entirely and
* exercise the legacy fallback path.
*/
Tff7b72private Tff7b72fun Td2a8ffrunPreflightTestTb4b4b4(
Te6edf3targetTb4b4b4: Tffa657String Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessageTb4b4b4: Tffa657String? Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3configureTb4b4b4: Te6edf3TestEsp32OtaUpdateEnvironmentTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3runUpdateTb4b4b4: Te6edf3suspend Te6edf3PreflightFixtureTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{
Te6edf3handlerTb4b4b4.Te6edf3startUpdateTb4b4b4(Te6edf3releaseTb4b4b4, Te6edf3hardwareTb4b4b4, Te6edf3targetTb4b4b4, Te6edf3statesTff7b72::Te6edf3addTb4b4b4, Te6edf3firmwareUriTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3assertionsTb4b4b4: Te6edf3PreflightFixtureTb4b4b4.Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tff7b72= Te6edf3runBlocking Tb4b4b4{
Tff7b72val Te6edf3radioController Tff7b72= Te6edf3FakeRadioControllerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3nodeRepository Tff7b72= Te6edf3newNodeRepositoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3firmwareUpdateStatusRepository Tff7b72= Te6edf3FirmwareUpdateStatusRepositoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3environment Tff7b72= Te6edf3TestEsp32OtaUpdateEnvironmentTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3applyTb4b4b4(Te6edf3configureTb4b4b4)
Tff7b72val Te6edf3handler Tff7b72=
Te6edf3makeHandlerTb4b4b4(
Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3fileHandler Tff7b72= Te6edf3newFileHandlerTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3firmwareUpdateStatusRepository Tff7b72= Te6edf3firmwareUpdateStatusRepositoryTb4b4b4,
Te6edf3environment Tff7b72= Te6edf3environmentTb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3states Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3FirmwareUpdateStateTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3fixture Tff7b72= Te6edf3PreflightFixtureTb4b4b4(Te6edf3handler Tff7b72= Te6edf3handlerTb4b4b4, Te6edf3radioController Tff7b72= Te6edf3radioControllerTb4b4b4, Te6edf3states Tff7b72= Te6edf3statesTb4b4b4)

T8b949e// Deterministically publish the simulated firmware confirmation from inside requestRebootOta, which the
T8b949e// handler invokes AFTER clearing stale notifications and capturing the baseline (see performUpdate). This
T8b949e// makes the notification arrive after those steps on every dispatcher β€” no fixed-delay sibling coroutine
T8b949e// that can fire too early (or too late) under slower host timing.
Te6edf3confirmationMessageTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3msg Tff7b72-Tff7b72>
Te6edf3radioControllerTb4b4b4.Te6edf3onRequestRebootOta Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3_Tb4b4b4, Te6edf3_Tb4b4b4, Te6edf3_ Tff7b72-Tff7b72>
Te6edf3radioControllerTb4b4b4.Te6edf3setClientNotificationTb4b4b4(Te6edf3ClientNotificationTb4b4b4(Te6edf3message Tff7b72= Te6edf3msgTb4b4b4)Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Te6edf3fixtureTb4b4b4.Te6edf3runUpdateTb4b4b4(Tb4b4b4)

Te6edf3fixtureTb4b4b4.Te6edf3assertionsTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(
Te6edf3firmwareUpdateStatusRepositoryTb4b4b4.Te6edf3statusTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3isOtaUpdateActiveTb4b4b4,
Ta5d6ff"Ta5d6ffFirmware update status must reset after the handler finishes or is cancelledTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e// ── Confirmed ──────────────────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed preflight releases the mesh transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3runUpdate Tff7b72= Tb4b4b4{
T8b949e// Bound the call: we only need to observe the preflight side effect. The post-preflight BLE transport
T8b949e// phase would otherwise burn ~30 s of real wall-clock time on retry/delay loops with mocked BLE deps.
Te6edf3withTimeoutOrNullTb4b4b4(T79c0ff3T79c0ff0T79c0ff0T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3handlerTb4b4b4.Te6edf3startUpdateTb4b4b4(Te6edf3releaseTb4b4b4, Te6edf3hardwareTb4b4b4, Te6edf3BLE_TARGETTb4b4b4, Te6edf3statesTff7b72::Te6edf3addTb4b4b4, Te6edf3firmwareUriTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed BLE preflight retries with fresh transport after connection failure`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transports Tff7b72=
Te6edf3ArrayDequeTff7b72<Te6edf3FakeOtaTransportTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3addTb4b4b4(
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6fffirstTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffOTA service missingTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3addTb4b4b4(Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffsecondTa5d6ff"Tb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3bleTransportFactory Tff7b72= Tb4b4b4{ Te6edf3transportsTb4b4b4.Te6edf3removeFirstTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:firstTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffFailed transport must be closed before retryTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffRetry must create and connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:firstTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:secondTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffSuccessful transport must be closed after transferTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffLater fresh transport should complete updateTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight waits for OTA service readiness before connecting`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= Te6edf3TEST_WIFI_READINESS_DELAY_MS
Te6edf3delayBlock Tff7b72= Tb4b4b4{ Te6edf3delayMs Tff7b72-Tff7b72> Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3delayMsTa5d6ff" Tb4b4b4}
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifiTa5d6ff"Tb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffWiFi post-confirm readiness delay must runTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifiTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi transport must connect after readiness delayTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifiTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffWiFi transport should connect after readiness delayTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight uses UDP discovered address before connecting`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= Te6edf3TEST_WIFI_READINESS_DELAY_MS
Te6edf3delayBlock Tff7b72= Tb4b4b4{ Te6edf3delayMs Tff7b72-Tff7b72> Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3delayMsTa5d6ff" Tb4b4b4}
Te6edf3wifiDiscoveryEnabled Tff7b72= Tff7b72true
Te6edf3discoverWifiOtaDeviceBlock Tff7b72= Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"
Te6edf3DISCOVERED_WIFI_TARGET
Tb4b4b4}
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3targetTa5d6ff"
Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Te6edf3targetTb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi OTA should listen for the loader's DHCP addressTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3DISCOVERED_WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffDiscovered DHCP address must replace the pre-reboot addressTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3DISCOVERED_WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffWiFi transport must connect to the discovered addressTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3WIFI_TARGETTa5d6ff" Tff7b72|Tff7b72| Tffa657it Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3WIFI_TARGETTa5d6ff" Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffConfigured pre-reboot address should not be used after discovery succeedsTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff" Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffDiscovery timeout window counts as the post-confirm readiness waitTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight falls back to configured address when UDP discovery misses`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= Te6edf3TEST_WIFI_READINESS_DELAY_MS
Te6edf3delayBlock Tff7b72= Tb4b4b4{ Te6edf3delayMs Tff7b72-Tff7b72> Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3delayMsTa5d6ff" Tb4b4b4}
Te6edf3wifiDiscoveryEnabled Tff7b72= Tff7b72true
Te6edf3discoverWifiOtaDeviceBlock Tff7b72= Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"
Tff7b72null
Tb4b4b4}
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3targetTa5d6ff"
Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Te6edf3targetTb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdiscoverTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi OTA should attempt UDP discovery before fixed-IP fallbackTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fffactory:Tffd700$Te6edf3WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffMissing discovery must fall back to configured targetTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffFallback transport must still connectTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffDiscovery miss must still apply the post-confirm readiness marginTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffdelay:Tffd700$Te6edf3TEST_WIFI_READINESS_DELAY_MSTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3WIFI_TARGETTa5d6ff"Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffReadiness delay must run before connectingTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight retries with fresh transport after connection failure`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3transports Tff7b72=
Te6edf3ArrayDequeTff7b72<Te6edf3FakeOtaTransportTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3addTb4b4b4(
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-firstTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffConnection refusedTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3addTb4b4b4(Te6edf3FakeOtaTransportTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-secondTa5d6ff"Tb4b4b4, Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{ Te6edf3transportsTb4b4b4.Te6edf3removeFirstTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:wifi-firstTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffFailed WiFi transport must be closed before retryTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifi-secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffWiFi retry must create and connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:wifi-firstTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:wifi-secondTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:wifi-secondTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffSuccessful WiFi transport must be closed after transferTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffLater fresh WiFi transport should complete updateTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed WiFi preflight closes failed transports and surfaces connection error after retries`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3attempt Tff7b72= T79c0ff0
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3WIFI_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3WIFI_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiOtaReadinessDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3wifiTransportFactory Tff7b72= Tb4b4b4{
Te6edf3attempt Tff7b72+Tff7b72= T79c0ff1
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-Tffd700$Te6edf3attemptTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffConnection refusedTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3WIFI_CONNECT_ATTEMPTSTb4b4b4, Te6edf3attemptTb4b4b4, Ta5d6ff"Ta5d6ffWiFi should use the full post-confirm retry budgetTa5d6ff"Tb4b4b4)
Te6edf3repeatTb4b4b4(Te6edf3WIFI_CONNECT_ATTEMPTSTb4b4b4) Tb4b4b4{ Te6edf3index Tff7b72-Tff7b72>
Tff7b72val Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffwifi-Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff"
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must close its failed transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstart:Ta5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstream:Ta5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3ErrorTff7b72>Tb4b4b4(Te6edf3statesTb4b4b4.Te6edf3lastOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Confirmed BLE preflight closes failed transports and surfaces connection error after retries`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3attempt Tff7b72= T79c0ff0
Te6edf3runPreflightTestTb4b4b4(
Te6edf3target Tff7b72= Te6edf3BLE_TARGETTb4b4b4,
Te6edf3confirmationMessage Tff7b72= Te6edf3BLE_CONFIRMATIONTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Te6edf3otaTransportRetryDelayMs Tff7b72= T79c0ff0L
Te6edf3bleTransportFactory Tff7b72= Tb4b4b4{
Te6edf3attempt Tff7b72+Tff7b72= T79c0ff1
Te6edf3FakeOtaTransportTb4b4b4(
Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffble-Tffd700$Te6edf3attemptTa5d6ff"Tb4b4b4,
Te6edf3events Tff7b72= Te6edf3eventsTb4b4b4,
Te6edf3connectResult Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3failureTb4b4b4(Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffGATT unreachableTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffConfirmed preflight must disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3BLE_CONNECT_ATTEMPTSTb4b4b4, Te6edf3attemptTb4b4b4, Ta5d6ff"Ta5d6ffBLE should use the full post-confirm retry budgetTa5d6ff"Tb4b4b4)
Te6edf3repeatTb4b4b4(Te6edf3BLE_CONNECT_ATTEMPTSTb4b4b4) Tb4b4b4{ Te6edf3index Tff7b72-Tff7b72>
Tff7b72val Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffble-Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff"
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must connect a fresh transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAttempt Tffd700${Te6edf3index Tff7b72+ T79c0ff1Tffd700}Ta5d6ff must close its failed transportTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4) Tff7b72< Te6edf3eventsTb4b4b4.Te6edf3indexOfTb4b4b4(Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3eventsTb4b4b4.Te6edf3none Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstart:Ta5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Ta5d6ff"Ta5d6ffstream:Ta5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3ErrorTff7b72>Tb4b4b4(Te6edf3statesTb4b4b4.Te6edf3lastOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}

T8b949e// ── Rejected ───────────────────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`Rejected preflight preserves mesh transport and surfaces Error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runPreflightTestTb4b4b4(
T8b949e// OTA-keyed message that is NOT the canonical "Rebooting to <mode> OTA" success prefix β†’ Rejected.
Te6edf3confirmationMessage Tff7b72= Ta5d6ff"Ta5d6ffCannot start OTA: OTA Loader partition not found.Ta5d6ff"Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6ffRejected preflight must NOT disconnect mesh serviceTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3statesTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72is Te6edf3FirmwareUpdateStateTb4b4b4.Te6edf3Error Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffRejected preflight must emit Error stateTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e// ── Legacy fallback ────────────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`Silent preflight falls back to legacy OTA reconnect path`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runPreflightTestTb4b4b4(
T8b949e// No emitter (confirmationMessage = null): older firmware may be silent, so the overridden preflight
T8b949e// timeout
T8b949e// should resolve quickly into the same disconnect/reconnect path the app used before preflight
T8b949e// confirmation.
Te6edf3confirmationMessage Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3configure Tff7b72= Tb4b4b4{
Te6edf3otaPreflightTimeoutMs Tff7b72= T79c0ff1T79c0ff0L
Te6edf3gattReleaseDelayMs Tff7b72= T79c0ff0L
Tb4b4b4}Tb4b4b4,
Te6edf3runUpdate Tff7b72= Tb4b4b4{
Te6edf3withTimeoutOrNullTb4b4b4(T79c0ff3T79c0ff0T79c0ff0T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3handlerTb4b4b4.Te6edf3startUpdateTb4b4b4(Te6edf3releaseTb4b4b4, Te6edf3hardwareTb4b4b4, Te6edf3BLE_TARGETTb4b4b4, Te6edf3statesTff7b72::Te6edf3addTb4b4b4, Te6edf3firmwareUriTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(
Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4,
Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4,
Ta5d6ff"Ta5d6ffSilent preflight must still disconnect mesh serviceTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
T8b949e// Valid BLE MAC target so startUpdate routes through the BLE OTA preflight path.
Tff7b72const Tff7b72val Te6edf3BLE_TARGET Tff7b72= Ta5d6ff"Ta5d6ffAA:BB:CC:DD:EE:FFTa5d6ff"
Tff7b72const Tff7b72val Te6edf3WIFI_TARGET Tff7b72= Ta5d6ff"Ta5d6ff192.168.1.33Ta5d6ff"
Tff7b72const Tff7b72val Te6edf3DISCOVERED_WIFI_TARGET Tff7b72= Ta5d6ff"Ta5d6ff192.168.1.44Ta5d6ff"
Tff7b72const Tff7b72val Te6edf3BLE_CONFIRMATION Tff7b72= Ta5d6ff"Ta5d6ffRebooting to BLE OTATa5d6ff"
Tff7b72const Tff7b72val Te6edf3WIFI_CONFIRMATION Tff7b72= Ta5d6ff"Ta5d6ffRebooting to WiFi OTATa5d6ff"
Tff7b72const Tff7b72val Te6edf3BLE_CONNECT_ATTEMPTS Tff7b72= T79c0ff5
Tff7b72const Tff7b72val Te6edf3WIFI_CONNECT_ATTEMPTS Tff7b72= T79c0ff1T79c0ff0
Tff7b72const Tff7b72val Te6edf3TEST_WIFI_READINESS_DELAY_MS Tff7b72= T79c0ff1T79c0ff2T79c0ff3T79c0ff4L
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72data Tff7b72class T56d364PreflightFixtureTb4b4b4(
Tff7b72val Te6edf3handlerTb4b4b4: Te6edf3Esp32OtaUpdateHandlerTb4b4b4,
Tff7b72val Te6edf3radioControllerTb4b4b4: Te6edf3FakeRadioControllerTb4b4b4,
Tff7b72val Te6edf3statesTb4b4b4: Te6edf3MutableListTff7b72<Te6edf3FirmwareUpdateStateTff7b72>Tb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72class T56d364TestEsp32OtaUpdateEnvironment Tb4b4b4: Te6edf3Esp32OtaUpdateEnvironment Tb4b4b4{
Tff7b72override Tff7b72var Te6edf3otaPreflightTimeoutMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff5Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3otaTransportRetryDelayMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff2Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3wifiOtaReadinessDelayMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff8Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3gattReleaseDelayMsTb4b4b4: Tffa657Long Tff7b72= T79c0ff1Te6edf3_000L
Tff7b72override Tff7b72var Te6edf3wifiDiscoveryEnabledTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

T8b949e// Fully qualified: an unqualified `delay(it)` would resolve to this class's own override (member functions win
T8b949e// over imported top-level functions), producing infinite recursion through delayBlock β†’ StackOverflowError.
Tff7b72var Te6edf3delayBlockTb4b4b4: Te6edf3suspend Tb4b4b4(Tffa657LongTb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{ Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3delayTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tff7b72var Te6edf3bleTransportFactoryTb4b4b4: Tb4b4b4(Tb4b4b4(Tffa657StringTb4b4b4) Tff7b72-Tff7b72> Te6edf3UnifiedOtaProtocolTb4b4b4)Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3wifiTransportFactoryTb4b4b4: Tb4b4b4(Tb4b4b4(Tffa657StringTb4b4b4) Tff7b72-Tff7b72> Te6edf3UnifiedOtaProtocolTb4b4b4)Tff7b72? Tff7b72= Tff7b72null
Tff7b72var Te6edf3discoverWifiOtaDeviceBlockTb4b4b4: Te6edf3suspend Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657String? Tff7b72= Tb4b4b4{ Tff7b72null Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffdelayTb4b4b4(Te6edf3millisecondsTb4b4b4: Tffa657LongTb4b4b4) Tb4b4b4{
Te6edf3delayBlockTb4b4b4(Te6edf3millisecondsTb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffcreateBleTransportTb4b4b4(
Te6edf3bleScannerTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3bleTb4b4b4.Te6edf3BleScannerTb4b4b4,
Te6edf3bleConnectionFactoryTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3bleTb4b4b4.Te6edf3BleConnectionFactoryTb4b4b4,
Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3dispatcherTb4b4b4: Te6edf3CoroutineDispatcherTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3UnifiedOtaProtocol Tff7b72=
Te6edf3bleTransportFactoryTff7b72?.Te6edf3invokeTb4b4b4(Te6edf3addressTb4b4b4) Tff7b72?: Te6edf3BleOtaTransportTb4b4b4(Te6edf3bleScannerTb4b4b4, Te6edf3bleConnectionFactoryTb4b4b4, Te6edf3addressTb4b4b4, Te6edf3dispatcherTb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffcreateWifiTransportTb4b4b4(Te6edf3deviceIpTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3UnifiedOtaProtocol Tff7b72=
Te6edf3wifiTransportFactoryTff7b72?.Te6edf3invokeTb4b4b4(Te6edf3deviceIpTb4b4b4) Tff7b72?: Te6edf3WifiOtaTransportTb4b4b4(Te6edf3deviceIpTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffdiscoverWifiOtaDeviceTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Te6edf3discoverWifiOtaDeviceBlockTb4b4b4.Te6edf3invokeTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72class T56d364FakeOtaTransportTb4b4b4(
Tff7b72private Tff7b72val Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4,
Tff7b72private Tff7b72val Te6edf3eventsTb4b4b4: Te6edf3MutableListTff7b72<Tffa657StringTff7b72>Tb4b4b4,
Tff7b72private Tff7b72val Te6edf3connectResultTb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3UnifiedOtaProtocol Tb4b4b4{
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffconnectTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffconnect:Tffd700$Te6edf3nameTa5d6ff"
Tff7b72return Te6edf3connectResult
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffstartOtaTb4b4b4(
Te6edf3sizeBytesTb4b4b4: Tffa657LongTb4b4b4,
Te6edf3sha256HashTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3onHandshakeStatusTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3OtaHandshakeStatusTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstart:Tffd700$Te6edf3nameTa5d6ff"
Tff7b72return Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffstreamFirmwareTb4b4b4(
Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4,
Te6edf3chunkSizeTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3onProgressTb4b4b4: Te6edf3suspend Tb4b4b4(Tffa657FloatTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstream:Tffd700$Te6edf3nameTa5d6ff"
Te6edf3onProgressTb4b4b4(T79c0ff1fTb4b4b4)
Tff7b72return Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3events Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffclose:Tffd700$Te6edf3nameTa5d6ff"
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.13s